Range = {}


Range.DebugMod = false
Range.ZoneIndex = 1

Range.MonitorRepeatTime = 5
Range.playerSpeedHintRepeatTime = 10
Range.DrawMapMonitorRepeatTime = 5

Range.TotalTargetNum = 12
Range.TotalRedNum = 8
Range.TotalBlueNum = Range.TotalTargetNum-Range.TotalRedNum
Range.TotalStaticNum = Range.TotalTargetNum/2
Range.TotalMovingNum = Range.TotalTargetNum/2

Range.WriteFile = false
Range.WriteFilePath = 'Missions/Buta/Apache_Training_Range/Saves/'
if lfs and io then
    Range.WriteFile = true
end

Range.SpawnedGroups_Default = {
    [coalition.side.RED] = {},
    [coalition.side.BLUE] = {},
}
Range.PlayerData_Default = {
    startTime = nil,
    enemyKilled = 0,
    allyKilled = 0,
    cloesestEnemyDistance = 15000,
}

Range.TargetTemplates = {
    [coalition.side.RED] = {},
    [coalition.side.BLUE] = {},
}

Range.SpawnCoalition = {
    [coalition.side.RED] = country.id.CJTF_RED,
    [coalition.side.BLUE] = country.id.CJTF_BLUE,
}

Range.AllRangeZones = {}
do
    -- @param table DCS_triggerZone or string triggerZone_Name
	-- @return Range object
    function Range:New(rangeZone)
        if not rangeZone then return end

        if Range.DebugMod then
            Utils.Debug('Range:New')
        end

        local inputType = type(rangeZone)

        if Range.DebugMod then
            Utils.Debug('Range:New() - inputType: '..inputType)
        end

        if inputType ~= 'string' and inputType ~= 'table' then 

            if Range.DebugMod then
                Utils.Debug('Range:New() - input invalid')
            end

            return 
        end

        local obj = {}
        
        local polygonPoints = Range.ZoneToPolygon(rangeZone)
        if not polygonPoints then return end
        obj.zonePolygon = polygonPoints

        obj.center = rangeZone.point
        obj.radius = rangeZone.radius

        obj.zoneName = 'RangeZone-'..Range.ZoneIndex
        Range.ZoneIndex = Range.ZoneIndex + 1

        if inputType == 'string' then
            local tempZone = trigger.action.getZone(rangeZone)
            if not tempZone then return end

            local polygonPoints = Range.ZoneToPolygon(rangeZone)
            if not polygonPoints then return end

            obj.zoneName = rangeZone
            obj.zonePolygon = polygonPoints
        end

        setmetatable(obj, self)
        self.__index = self

        if Range.DebugMod then
            Utils.Debug('New Range: '..obj.zoneName)
        end

        Range.AllRangeZones[obj.zoneName] = obj
        obj:Init()

        return obj
    end

    function Range:Init(playerUnit,notdrawZone)
        self.playerUnit = playerUnit
        self.playSound = true

        if playerUnit then
            self:CreatePlayerMenu(self.playerUnit)
        end

        if not notdrawZone then
            if self.assignedRangeZoneDrawID then

                if not self.playerUnit then
                    trigger.action.setMarkupColorFill(self.assignedRangeZoneDrawID,{0,1,0,0.2})
                    trigger.action.setMarkupTypeLine(self.assignedRangeZoneDrawID,2)
                end

                if self.playerUnit then
                    trigger.action.setMarkupColorFill(self.assignedRangeZoneDrawID,{1,0,0,0.2})
                    trigger.action.setMarkupTypeLine(self.assignedRangeZoneDrawID,1)
                end
            end
            
            if self.assignedRangeZoneDrawID then
                if not self.playerUnit then
                    trigger.action.setMarkupText(self.assignedRangeZoneTextID, '靶场: '..self.zoneName..' 当前使用: 无')
                end
                if self.playerUnit then
                    trigger.action.setMarkupText(self.assignedRangeZoneTextID, '靶场: '..self.zoneName..' 当前使用: '..self.playerName)
                end
            end

            if not self.assignedRangeZoneDrawID then
                self.assignedRangeZoneDrawID = Utils.getMarkID()
                Utils.drawPolygon(-1,self.assignedRangeZoneDrawID,self.zonePolygon,{1,0,0,0.2},{0,1,0,0.2},2)
            end

            if not self.assignedRangeZoneTextID then
                self.assignedRangeZoneTextID = Utils.getMarkID()
                trigger.action.textToAll(-1,self.assignedRangeZoneTextID,self.zonePolygon[1],{0,0,0,1},{1,1,1,0.5},12,false,'靶场: '..self.zoneName..' 当前使用: 无')
            end
        end

        if Range.DebugMod then
            Utils.Debug('playerData')
            Utils.Debug(self.playerData)
        end
        
        self.EndTraining({context = self,notShowResults = true})
        self.spawnedGroups = Utils.deepCopy(Range.SpawnedGroups_Default) 
        self.playerData = Utils.deepCopy(Range.PlayerData_Default)
    end

    function Range:Remove()
        if not Range.AllRangeZones[self.zoneName] then return end
        self.EndTraining({context = self,notShowResults = true})

        Range.AllRangeZones[self.zoneName] = nil
    end

    function Range.ZoneToPolygon(DCSZone)
        if not DCSZone then return end
        if not DCSZone.point or not DCSZone.radius then return end

        local length = DCSZone.radius
        
        local polygon = {
            { 
                x = DCSZone.point.x + length,
                z = DCSZone.point.z - length,
                y = 0
            },
            { 
                x = DCSZone.point.x + length,
                z = DCSZone.point.z + length,
                y = 0
            },
            { 
                x = DCSZone.point.x - length,
                z = DCSZone.point.z + length,
                y = 0
            },
            { 
                x = DCSZone.point.x - length,
                z = DCSZone.point.z - length,
                y = 0
            },
        }

        return polygon
    end

    function Range.CanBindPlayer(object)
        if not object then return end
        if not object:isExist() then return end

        local playerPoint = object:getPoint()

        for zoneName,zone in pairs(Range.AllRangeZones) do
            if Range.DebugMod then
                local Distance = Utils.get2DDist(playerPoint,zone.center)
                Utils.Debug(zoneName..' Distance: '..Distance)
            end

            if Utils.get2DDist(playerPoint,zone.center) <= zone.radius+7000 then
                if zone.playerUnit then return end
                return zone
            end
        end

        return
    end

    function Range:CreatePlayerMenu(playerUnit)
        if not playerUnit then return end
        if not playerUnit:isExist() then return end

        local playerGroup = playerUnit:getGroup()
        if not playerGroup then return end
        if not playerGroup:isExist() then return end

        local goupID = playerGroup:getID()

        if self.RefreshTargetsMenu then
            missionCommands.removeItemForGroup(goupID,self.RefreshTargetsMenu)
            self.RefreshTargetsMenu = nil
        end
        if self.EndTrainingMenu then
            missionCommands.removeItemForGroup(goupID,self.EndTrainingMenu)
            self.EndTrainingMenu = nil
        end

        self.RefreshTargetsMenu = missionCommands.addCommandForGroup(goupID,'刷新目标',nil,self.SpawnTargets,{context = self})
        self.EndTrainingMenu = missionCommands.addCommandForGroup(goupID,'结束训练',nil,self.EndTraining,{context = self,reason = '玩家手动结束'})
        self.SoundSwichMenu = missionCommands.addCommandForGroup(goupID,'提示音开关',nil,self.SoundSwich,{context = self})
    end

    function Range:GetRandSpawnPoint()
        for i=1,100,1 do
            -- local spawnPoint = Utils.getRandomPointInPoly(self.zonePolygon)
            local spawnPoint = Utils.getRandPointInCircle(self.center,8000)
            local landType =land.getSurfaceType(spawnPoint)
            if landType ~= land.SurfaceType.SHALLOW_WATER or landType ~= land.SurfaceType.WATER or landType ~= land.SurfaceType.RUNWAY then
                return spawnPoint
            end
        end

        return nil
    end

    function Range.setGroupGreen(Controller)
        if not Controller then return end

        Controller:setOption(AI.Option.Ground.id.ROE,AI.Option.Ground.val.ROE.WEAPON_HOLD)
        Controller:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
    end

    function Range:SpawnGroup(tCoalition,spawnData)
        if not tCoalition or not spawnData then return end

        if Range.DebugMod then
            Utils.Debug('Range:SpawnGroup: '..spawnData.name..' for coalition: '..tCoalition)
        end

        local newGroup = coalition.addGroup(self.SpawnCoalition[tCoalition],Group.Category.GROUND,spawnData)
        return newGroup
    end

    function Range:getTaskOnRoad(startPoint,endPoint,speed)
        --- 检测路径是否在靶场范围内
        -- 获取路径点
        local pathPoint = land.findPathOnRoads('roads',startPoint.x,startPoint.y,endPoint.x,endPoint.y)
        -- 筛选转向点
        local turnPoints = {}
        local baseHeading = Utils.getDirection(startPoint,pathPoint[1])
        for i, point in ipairs(pathPoint) do
            if i>2 then
                local newHeading = Utils.getDirection(pathPoint[i-1],pathPoint[i])
                if baseHeading ~= newHeading then
                    baseHeading = newHeading
                    table.insert(turnPoints,point)
                end
            end
        end
        -- 检查转向点是否有在靶场范围外的
        for i, turnPoint in ipairs(turnPoints) do
            if not Utils.pointInPolygon(turnPoint,self.zonePolygon) then
                return Range:getTaskOffRoad(startPoint,endPoint,speed)
            end
        end

        local RoadPoint_Start = {}
        RoadPoint_Start.x, RoadPoint_Start.y = land.getClosestPointOnRoads('roads',startPoint.x,startPoint.y)

        local RoadPoint_End = {}
        RoadPoint_End.x,RoadPoint_End.y = land.getClosestPointOnRoads('roads',endPoint.x,endPoint.y)

        local Mission = {
            id = 'Mission',
            params = {
                route = {
                    points = {
                        [1] = {
                            x = RoadPoint_Start.x,
                            y = RoadPoint_Start.y,
                            speed = speed,
                            type = AI.Task.WaypointType.TURNING_POINT,
                            action = AI.Task.VehicleFormation.ON_ROAD,
                        },
                        [2] = {
                            x = RoadPoint_End.x,
                            y = RoadPoint_End.y,
                            speed = speed,
                            type = AI.Task.WaypointType.TURNING_POINT,
                            action = AI.Task.VehicleFormation.ON_ROAD,
                            
                        },
                        [3] = {
                            x = endPoint.x,
                            y = endPoint.y,
                            speed = speed,
                            type = AI.Task.WaypointType.TURNING_POINT,
                            action = AI.Task.VehicleFormation.OFF_ROAD,
                        }
                    },
                },
            },
        }

        return Mission
    end

    function Range:getTaskOffRoad(startPoint,endPoint,speed)
        local Mission = {
            id = 'Mission',
            params = {
                route = {
                    points = {
                        [1] = {
                            x = startPoint.x,
                            y = startPoint.y,
                            speed = speed,
                            type = AI.Task.WaypointType.TURNING_POINT,
                            action = AI.Task.VehicleFormation.OFF_ROAD,
                        },
                        [2] = {
                            x = endPoint.x,
                            y = endPoint.y,
                            speed = speed,
                            type = AI.Task.WaypointType.TURNING_POINT,
                            action = AI.Task.VehicleFormation.OFF_ROAD,
                        }
                    },
                },
            },
        }

        return Mission
    end

    function Range:SpawnStatic(tCoalition,spawnNumber)
        if not tCoalition or not spawnNumber then return end
        if spawnNumber < 1 then return true end
        
        if Range.DebugMod then
            Utils.Debug('Range:SpawnStatic, tCoalition: '..tCoalition..' spawnNumber: '..spawnNumber)
        end

        local spawnedGroups = {}

        for i=1,spawnNumber,1 do
            local spawnPoint = self:GetRandSpawnPoint()

            if not spawnPoint then
                spawnPoint = {x=self.center.x,y=self.center.z}
            end

            local coalitionName = {
                [coalition.side.RED] = 'Red',
                [coalition.side.BLUE] = 'Blue',
            }

            local spawnData = {}
            spawnData.name = self.zoneName..'_'..coalitionName[tCoalition]..'_Static-'..i
            spawnData.task = "Ground Nothing"
            spawnData.units = {
                [1] = {
                    x = spawnPoint.x,
                    y = spawnPoint.y,
                    name = spawnData.name..'-1',
                    type = self.TargetTemplates[tCoalition][math.random(1,#self.TargetTemplates[tCoalition])]
                },
            }

            local newGroup = self:SpawnGroup(tCoalition,spawnData)
            if not newGroup then return end
            if not newGroup:isExist() then return end

            local Controller = newGroup:getController()
            timer.scheduleFunction(self.setGroupGreen,Controller,timer.getTime()+1)

            table.insert(self.spawnedGroups[tCoalition], newGroup)
        end 

        return true
    end
    function Range:SpawnMoving(tCoalition,spawnNumber)
        if not tCoalition or not spawnNumber then return end
        if spawnNumber < 1 then return true end

        if Range.DebugMod then
            Utils.Debug('Range:SpawnMoving, tCoalition: '..tCoalition..' spawnNumber: '..spawnNumber)
        end

        local pathTable = {
            {
                start = self.zonePolygon[1],
                destination = self.zonePolygon[3],
                startAngleMin = 90,
                destAngleMin = 270,
                add = 90,
            },
            {
                start = self.zonePolygon[2],
                destination = self.zonePolygon[4],
                startAngleMin = 180,
                destAngleMin = 0,
                add = 90,
            },
            -- {
            --     start = {x=self.zonePolygon[1].x-self.radius,y=self.zonePolygon[1].y},
            --     destination = {x=self.zonePolygon[1].x-self.radius,y=self.zonePolygon[1].y+self.radius*2},
            --     startAngleMin = 0,
            --     destAngleMin = 180,
            --     add = 180,
            -- },
            -- {
            --     start = {x=self.zonePolygon[1].x,y=self.zonePolygon[1].y+self.radius},
            --     destination = {x=self.zonePolygon[1].x-self.radius*2,y=self.zonePolygon[1].y+self.radius},
            --     startAngleMin = 90,
            --     destAngleMin = 270,
            --     add = 180,
            -- },
        }

        for i=1,spawnNumber,1 do
            local path = pathTable[math.random(1,#pathTable)]

            local spawnPoint = Utils.getRandPointInCircle(path.start,self.radius/2,100,path.startAngleMin+path.add,path.startAngleMin)

            if not spawnPoint then
                spawnPoint = {x=self.center.x,y=self.center.z}
            end
            
            local destinationPoint = Utils.getRandPointInCircle(path.destination,self.radius/2,100,path.destAngleMin+path.add,path.destAngleMin)

            local coalitionName = {
                [coalition.side.RED] = 'Red',
                [coalition.side.BLUE] = 'Blue',
            }

            local spawnData = {}
            spawnData.name = self.zoneName..'_'..coalitionName[tCoalition]..'_Moving-'..i
            spawnData.task = "Ground Nothing"
            spawnData.units = {
                [1] = {
                    x = spawnPoint.x,
                    y = spawnPoint.y,
                    name = spawnData.name..'-1',
                    type = self.TargetTemplates[tCoalition][math.random(1,#self.TargetTemplates[tCoalition])]
                },
            }

            local newGroup = self:SpawnGroup(tCoalition,spawnData)
            if not newGroup then return end
            if not newGroup:isExist() then return end

            local Controller = newGroup:getController()
            timer.scheduleFunction(self.setGroupGreen,Controller,timer.getTime()+1)

            local speed = math.random(20/3.6,60/3.6)
            local MovingMission = self:getTaskOnRoad(spawnPoint,destinationPoint,speed)
            timer.scheduleFunction(function (vars)
                local Controller = vars.Controller
                local Mission = vars.Mission
                if not Controller or not Mission then return end

                Controller:setTask(Mission)
            end,{Controller = Controller, Mission = MovingMission},timer.getTime()+1)

            table.insert(self.spawnedGroups[tCoalition], newGroup)

        end

        return true
    end

    function Range:DespawnTargets()
        self.spawnedGroups = self.spawnedGroups or Utils.deepCopy(Range.SpawnedGroups_Default)

        if Range.DebugMod then
            Utils.Debug('Range.DespawnTargets')
            Utils.Debug('self.spawnedGroups: ')
            Utils.Debug(self.spawnedGroups)
        end

        for tCoalition,groups in pairs(self.spawnedGroups) do
            for i,group in ipairs(groups) do
                if group and group.isExist then
                    if group:isExist() then

                        if Range.DebugMod then
                            Utils.Debug('destroy: '..group:getName())
                        end
                
                        group:destroy()
                    end
                end
            end
        end
    end
    
    function Range.Format_time(total_seconds)
        -- 确保三位小数格式并处理浮点精度问题
        local formatted = string.format("%.3f", total_seconds)
        local integer_part, decimal_part = formatted:match("^(%d+)%.(%d%d%d)$")
        local integer_seconds = tonumber(integer_part)
        local milliseconds = tonumber(decimal_part)
    
        -- 计算时分秒
        local hours = math.floor(integer_seconds / 3600)
        local remaining = integer_seconds % 3600
        local minutes = math.floor(remaining / 60)
        local seconds = remaining % 60
    
        -- 格式化输出为HH:MM:SS.sss
        -- return string.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds)
        return string.format("%02d:%02d:%02d", hours, minutes, seconds)
    end

    --[[
    函数：decimal_to_dms
    功能：将十进制度坐标转换为度分秒格式（带方向标识）
    参数：decimal (十进制度数值), is_latitude (是否为纬度)
    返回：格式如 "N 41°28'46.44"" 的字符串
    --]]
    function Range.Decimal_to_dms(decimal, is_latitude)
        local degrees = math.floor(math.abs(decimal))
        local remainder = (math.abs(decimal) - degrees) * 60
        local minutes = math.floor(remainder)
        local seconds =  math.floor((remainder - minutes)*1000)

        -- 确定方向标识
        local direction
        if is_latitude then
            direction = (decimal >= 0) and "N" or "S"
        else
            direction = (decimal >= 0) and "E" or "W"
        end

        -- 格式化输出（秒保留两位小数）
        return string.format("%s %02d°%02d.%03d\"", direction, degrees, minutes, seconds)
    end

    --@param vec3
    --@param type string which form you want return: 'MGRS'|'latlon' or 'LL'
    --@return string text of this coord
    function Range.Format_Coord(vec3,tType)
        if not vec3 then return end
        if not tType then return end

        
        local oriPoint = Utils.makeVec3(vec3)
    
        if tType == 'MGRS' then
            local lat,lon,alt = coord.LOtoLL(oriPoint)
            local MGRSTable = coord.LLtoMGRS(lat,lon)

            -- return MGRSTable.UTMZone .. ' ' .. MGRSTable.MGRSDigraph .. ' ' .. MGRSTable.Easting .. ' ' .. MGRSTable.Northing
            return string.format('%03s %s %05s %05s',MGRSTable.UTMZone,MGRSTable.MGRSDigraph,MGRSTable.Easting,MGRSTable.Northing)
        end

        if tType == 'latlon' or tType == 'LL' then
            local lat,lon,alt = coord.LOtoLL(oriPoint)

            return Range.Decimal_to_dms(lat,true)..' '..Range.Decimal_to_dms(lon)
        end
    end

    function Range.SpawnTargets(vars)
        local self = vars.context
        self:DespawnTargets()

        if Range.DebugMod then
            Utils.Debug('Range.SpawnTargets')
        end

        self.playerData = Utils.deepCopy(Range.PlayerData_Default)

        local RedStaticNumber = math.random(Range.TotalStaticNum-Range.TotalBlueNum,Range.TotalStaticNum)
        local RedMovingNumber = Range.TotalRedNum-RedStaticNumber

        local BlueStaticNumber = Range.TotalStaticNum-RedStaticNumber
        local BlueMovingNumber = Range.TotalBlueNum-BlueStaticNumber

        local RedStatic = self:SpawnStatic(coalition.side.RED,RedStaticNumber)
        local RedMoving = self:SpawnMoving(coalition.side.RED,RedMovingNumber)
        
        local BlueStatic = self:SpawnStatic(coalition.side.BLUE,BlueStaticNumber)
        local BlueMoving = self:SpawnMoving(coalition.side.BLUE,BlueMovingNumber)

        if RedStatic and RedMoving and BlueStatic and BlueMoving then
            local playerUnit = self.playerUnit
            if not playerUnit then
                self:Init()
                return
            end
            if not playerUnit:isExist() then
                self:Init()
                return
            end

            self.playerData.startTime = timer.getAbsTime()
            local startTime = Range.Format_time(self.playerData.startTime)

            local msg = ''
            msg = msg..'==================================\n'
            msg = msg..'    训练开始, 开始时间: '..startTime..' (游戏内)\n'
            msg = msg..'==================================\n'
            msg = msg..'           靶场范围坐标:\n'
            msg = msg..'1: MGRS: '..Range.Format_Coord(self.zonePolygon[1],'MGRS')..' | '..Range.Format_Coord(self.zonePolygon[1],'LL')..'\n'
            msg = msg..'2: MGRS: '..Range.Format_Coord(self.zonePolygon[2],'MGRS')..' | '..Range.Format_Coord(self.zonePolygon[2],'LL')..'\n'
            msg = msg..'3: MGRS: '..Range.Format_Coord(self.zonePolygon[3],'MGRS')..' | '..Range.Format_Coord(self.zonePolygon[3],'LL')..'\n'
            msg = msg..'4: MGRS: '..Range.Format_Coord(self.zonePolygon[4],'MGRS')..' | '..Range.Format_Coord(self.zonePolygon[4],'LL')..'\n'
            trigger.action.outTextForUnit(playerUnit:getID(),msg,300,true)

            local playerName = playerUnit:getPlayerName()
            trigger.action.setMarkupText(self.assignedRangeZoneTextID, '靶场: '..self.zoneName..' 当前使用: '..playerName)
            trigger.action.setMarkupColorFill(self.assignedRangeZoneDrawID,{1,0,0,0.2})
            trigger.action.setMarkupTypeLine(self.assignedRangeZoneDrawID,1)

            --Sound
            if self.playSound then
                trigger.action.outSoundForUnit(playerUnit:getID(),'audio/SFX/success.ogg')
            end

            if Range.DebugMod then
                Utils.Debug('timer.getTime(): '..Range.Format_time(timer.getTime()))
                Utils.Debug('timer.getAbsTime(): '..Range.Format_time(timer.getAbsTime()))
            end

            self:Start()
        end

    end

    function Range.EndTraining(vars)
        local self = vars.context
        local notShowResults = vars.notShowResults
        local reason = vars.reason
        local Error = vars.Error

        if Range.DebugMod then
            Utils.Debug('Range.EndTraining')
        end

        self:Stop()
        self:DespawnTargets()
        if not notShowResults then self:ShowResults(reason,Error) end
        
    end

    function Range.SoundSwich(vars)
        local self = vars.context
        self.playSound = not self.playSound

        
        local playerUnit = self.playerUnit
        if not playerUnit then
            self:Init()
            return
        end
        if not playerUnit:isExist() then
            self:Init()
            return
        end

        if self.playSound then
            trigger.action.outTextForUnit(playerUnit:getID(),'音效: 开',5)
        else
            trigger.action.outTextForUnit(playerUnit:getID(),'音效: 关',5)
        end
    end

    --@param table or groupName
    --@return nil
    function Range.AddTargetType(targetGroup,coalition)
        if not targetGroup then return end
        local tempGroup = targetGroup
        if type(targetGroup) == 'string' then
            tempGroup = Group.getByName(targetGroup)
        end
        if not tempGroup then return end
        if not tempGroup.getCoalition then return end

        local coalition = coalition or tempGroup:getCoalition()

        local units = tempGroup:getUnits()
        for i,unit in ipairs(units) do
            table.insert(Range.TargetTemplates[coalition],unit:getTypeName())
        end
    end

    function Range:writeFile(reason)
        if not Range.WriteFile then 
            env.info('Range.writeFile() - lfs Not avaulible')
            return 
        end

        if not io then
            net.log("Range.writeFile() - io unavailable, fail to write RangeData")
            return
        end

        local playerUnit = self.playerUnit
        if not playerUnit then
            self:Init()
            return
        end
        if not playerUnit:isExist() then
            self:Init()
            return
        end

        local playerName = playerUnit:getPlayerName()

        local dir_path = lfs.writedir()..Range.WriteFilePath..playerName..'/'
        local fildName = 'Data_'..playerName

        local ok, err = lfs.mkdir(dir_path)
        if not ok then
            env.info('Range:writeFile() - fail to create folder: '..dir_path)
        end

        local startTime = Range.Format_time(self.playerData.startTime)
        local usedTime = (math.floor((timer.getAbsTime() - self.playerData.startTime)*10)/10)..'s'
        local enemyKilled = self.playerData.enemyKilled
        local allyKilled = self.playerData.allyKilled
        local cloesestEnemyDistanceTest = (math.floor(self.playerData.cloesestEnemyDistance*10)/10)..'米'
        if self.playerData.cloesestEnemyDistance >= 15000 then
            cloesestEnemyDistanceTest = '非常远.'
        end

        local reason_text = '无'
        if reason then reason_text = reason end

        -- local txt_file = io.open(dir_path.."/"..fildName..".txt","w")
        -- local txt_data = {
        --     ['玩家'] = playerName,
        --     ['开始时间'] = startTime,
        --     ['持续时间'] = usedTime,
        --     ['击杀敌方目标'] = enemyKilled,
        --     ['击杀友方目标'] = allyKilled,
        --     ['敌方最近时的距离'] = cloesestEnemyDistanceTest,
        --     ['结束原因'] = reason_text,
        -- }
        -- txt_data = net.lua2json(txt_data)

        -- txt_file:write(txt_data)
        -- txt_file:close()
        
        local csv_file = io.open(dir_path.."/"..fildName..".csv","w")
        local csv_data = "玩家,开始时间,持续时间,击杀敌方目标,击杀友方目标,敌方最近时的距离,结束原因\n"
        
        csv_data = csv_data..playerName..','
        csv_data = csv_data..startTime..','
        csv_data = csv_data..usedTime..','
        csv_data = csv_data..enemyKilled..','
        csv_data = csv_data..allyKilled..','
        csv_data = csv_data..cloesestEnemyDistanceTest..','
        csv_data = csv_data..reason_text


        csv_file:write(csv_data)
        csv_file:close()
        
        env.info("Range:writeFile() - write successful at: "..dir_path)
    
    end

    function Range:ShowResults(reason,Error)
        local playerUnit = self.playerUnit
        if not playerUnit then
            self:Init()
            return
        end
        if not playerUnit:isExist() then
            self:Init()
            return
        end
        
        local msg = ''
        msg = msg..'=================================='
        msg = msg..'\n           本次训练结果: \n'
        msg = msg..'=================================='

        if self.playerData.startTime then
            msg = msg..'\n开始时间: '..Range.Format_time(self.playerData.startTime)
            msg = msg..'\n持续时间: '..(math.floor((timer.getAbsTime() - self.playerData.startTime)*10)/10)..'s'
        end

        msg = msg..'\n击杀敌方目标: '..self.playerData.enemyKilled
        msg = msg..'\n击杀友方目标: '..self.playerData.allyKilled

        local cloesestEnemyDistanceTest = (math.floor(self.playerData.cloesestEnemyDistance*10)/10)..'米'
        if self.playerData.cloesestEnemyDistance >= 15000 then
            cloesestEnemyDistanceTest = '非常远.'
        end
        msg = msg..'\n敌方最近时的距离: '..cloesestEnemyDistanceTest
        msg = msg..'\n=================================='

        if reason then
            msg = msg..'\n           训练结束: '..reason
            msg = msg..'\n=================================='
        end

        trigger.action.outTextForUnit(playerUnit:getID(),msg,60,true)
        --Sound
        if self.playSound then
            if not Error then
                trigger.action.outSoundForUnit(playerUnit:getID(),'audio/SFX/success.ogg')
            end

            if Error then
                trigger.action.outSoundForUnit(playerUnit:getID(),'audio/SFX/fail.ogg')
            end
        end

        self:writeFile(reason)
    end

    local ev = {}
    function ev:onEvent(event)
        if event.id == world.event.S_EVENT_BIRTH then
            -- local rangeObj = self.listeningZone
            local object = event.initiator
            if not object:isExist() then return end

            local SpawnPoint = object:getPoint()

            if not object.getPlayerName then return end
            local playerName = object:getPlayerName()
            if not playerName then return end

            local rangeZone = Range.CanBindPlayer(object)

            if rangeZone then
                rangeZone.playerName = playerName

                rangeZone:Init(object)
                trigger.action.outText('靶场: '..rangeZone.zoneName..' 已分配给玩家: '..playerName,10)

                local msg = ''
                msg = msg..'===================================\n'
                msg = msg..'           靶场范围坐标:\n'
                msg = msg..'===================================\n'
                msg = msg..'1: MGRS: '..Range.Format_Coord(rangeZone.zonePolygon[1],'MGRS')..' | '..Range.Format_Coord(rangeZone.zonePolygon[1],'LL')..'\n'
                msg = msg..'2: MGRS: '..Range.Format_Coord(rangeZone.zonePolygon[2],'MGRS')..' | '..Range.Format_Coord(rangeZone.zonePolygon[2],'LL')..'\n'
                msg = msg..'3: MGRS: '..Range.Format_Coord(rangeZone.zonePolygon[3],'MGRS')..' | '..Range.Format_Coord(rangeZone.zonePolygon[3],'LL')..'\n'
                msg = msg..'4: MGRS: '..Range.Format_Coord(rangeZone.zonePolygon[4],'MGRS')..' | '..Range.Format_Coord(rangeZone.zonePolygon[4],'LL')..'\n'
                trigger.action.outTextForUnit(object:getID(),msg,300)
                
                if Range.DebugMod then
                    Utils.Debug('Bind player: '..playerName..' to Zone: '..rangeZone.zoneName)
                end
            end
        end

        if event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT then
            local initiator = event.initiator
            if not initiator then return end
            if not initiator:isExist() then return end
            if not initiator.getPlayerName then return end
            
            local playerName = initiator:getPlayerName()
            if not playerName then return end

            for zoneName,zone in pairs(Range.AllRangeZones) do
                if zone.playerName then
                    if playerName == zone.playerName then

                        zone:Init()
                        return
                    end
                end
            end
        end

        -- if event.id == world.event.S_EVENT_KILL then
        --     local initiator = event.initiator
        --     if not initiator then return end
        --     if not initiator:isExist() then return end
        --     if not initiator.getPlayerName then return end
            
        --     local playerName = initiator:getPlayerName()
        --     if not playerName then return end

        --     local targetUnit = event.target
        --     if not targetUnit then return end
        --     local tCoalition = targetUnit:getCoalition()
        --     local pCoalition = initiator:getCoalition()

        --     local otherSide = {
        --         [coalition.side.BLUE] = coalition.side.RED,
        --         [coalition.side.RED] = coalition.side.BLUE,
        --     }

        --     for zoneName,zone in pairs(Range.AllRangeZones) do
        --         if zone.playerName then
        --             if playerName == zone.playerName then
        --                 if tCoalition == otherSide[pCoalition] then
        --                     zone.playerData.enemyKilled = zone.playerData.enemyKilled + 1

        --                     Utils.Debug('enemyKilled: '..zone.playerData.enemyKilled)
        --                     if Range.DebugMod then
        --                     end

        --                     return
        --                 end
        --                 if tCoalition == pCoalition then
        --                     zone.playerData.allyKilled = zone.playerData.allyKilled + 1
                            
        --                     Utils.Debug('allyKilled: '..zone.playerData.allyKilled)
        --                     if Range.DebugMod then
        --                     end

        --                     return
        --                 end
        --             end
        --         end
        --     end

        -- end
    end
    world.addEventHandler(ev)

    local hitEvent = {
        AddHandlerTimerDelay = 1, --Second
    }
    function hitEvent:AddEventHandler(Delay)
        local tDelay = Delay or self.AddHandlerTimerDelay
        
        timer.scheduleFunction(function (eventHandler)
            world.addEventHandler(eventHandler)
        end,self,timer.getTime() + tDelay)
    end

    function hitEvent:isUnitAlive(unit)
        if not unit then return false end

        if not unit:isExist() then return false end

        if unit:getLife() < 1 then return false end

        return true
    end

    function hitEvent:onEvent(event)
        if event.id == world.event.S_EVENT_HIT then
            local initiator = event.initiator
            if not initiator then return end
            if not initiator:isExist() then return end
            if not initiator.getPlayerName then return end
            
            local playerName = initiator:getPlayerName()
            if not playerName then return end

            local targetUnit = event.target
            if not targetUnit then return end
            if not targetUnit.getCoalition then return end
            if not initiator.getCoalition then return end

            local tCoalition = targetUnit:getCoalition()
            local pCoalition = initiator:getCoalition()

            local otherSide = {
                [coalition.side.BLUE] = coalition.side.RED,
                [coalition.side.RED] = coalition.side.BLUE,
            }

            for zoneName,zone in pairs(Range.AllRangeZones) do
                if zone.playerName then
                    if playerName == zone.playerName then
                        if tCoalition == otherSide[pCoalition] then
                            if not self:isUnitAlive(targetUnit) then
                                zone.playerData.enemyKilled = zone.playerData.enemyKilled + 1
                                trigger.action.outTextForUnit(initiator:getID(),'击杀敌方目标: '..targetUnit:getTypeName(),2)
                                
                                world.removeEventHandler(hitEvent)
                                self:AddEventHandler()
                                return
                            end
                        end
                        if tCoalition == pCoalition then
                            if not self:isUnitAlive(targetUnit) then
                                zone.playerData.allyKilled = zone.playerData.allyKilled + 1
                                trigger.action.outTextForUnit(initiator:getID(),'击杀友方目标: '..targetUnit:getTypeName(),2)

                                world.removeEventHandler(hitEvent)
                                self:AddEventHandler()
                                return
                            end
                        end
                    end
                end
            end
        end
    end
    world.addEventHandler(hitEvent)

    function Range._MonitorFunction(vars,time)
        local self = vars.context
        local repeatTime = vars.repeatTime

        if Range.DebugMod then
            Utils.Debug('Range._MonitorFunction')
        end

        local playerUnit = self.playerUnit
        if not playerUnit then
            self:Init()
            return
        end
        if not playerUnit:isExist() then
            self:Init()
            return
        end

        local playerGroup = playerUnit:getGroup()
        if not playerGroup then
            self:Init()
            return
        end
        if not playerGroup:isExist() then
            self:Init()
            return
        end
        
        -- 玩家从任意一个方向进入靶场
        local playerPoint = playerUnit:getPoint()
        if not playerPoint then
            self:Init()
            Utils.Debug('脚本意外终止 - 可能原因: 无法获取玩家位置.')
            return
        end
        if Utils.pointInPolygon(playerPoint,self.zonePolygon) then
            self.EndTraining({context = self,reason = '玩家进入了靶场',Error = true})
            return
        end

        -- 更新最近距离
        local targetIsExist = false

        if Range.DebugMod then
            Utils.Debug('self.spawnedGroups:')
            Utils.Debug(self.spawnedGroups)
        end

        for tCoalition,group in pairs(self.spawnedGroups[coalition.side.RED]) do
            if group and group.isExist then
                if group:isExist() then
                    if Range.DebugMod then
                        Utils.Debug('group: '..group:getName())
                    end

                    for j, unit in ipairs(group:getUnits()) do
                        if unit:isExist() then
                            if Range.DebugMod then
                                Utils.Debug('unit: '..unit:getName())
                            end
                            if unit:getLife() >= 1 then
                                targetIsExist = true

                                local targetPoint = unit:getPoint()
                                local distance = Utils.get3DDist(playerPoint,targetPoint)
                                if distance < self.playerData.cloesestEnemyDistance then
                                    self.playerData.cloesestEnemyDistance = distance
                                end
                            end
                        end
                    end
                end
            end
        end

        -- 所有敌方目标被摧毁
        if not targetIsExist then
            self.EndTraining({context = self,reason = '所有敌方目标被摧毁'})
            return
        end

        -- 玩家飞行速度低于40节
        if playerPoint.y-land.getHeight({x=playerPoint.x,y=playerPoint.z}) > 100 then
            local playerSpeed = Utils.mag(playerUnit:getVelocity())
            if not playerSpeed then
                self:Init()
                Utils.Debug('脚本意外终止 - 可能原因: 无法获取玩家速度.')
                return
            end
            
            self.playerSpeedHint = self.playerSpeedHint or Range.playerSpeedHintRepeatTime

            if self.playerSpeedHint <= 0 then
                if playerSpeed <= 40/1.944 then
                    trigger.action.outTextForUnit(playerUnit:getID(),'============\n速度小于 40 节, 当前速度: '..(math.floor((playerSpeed*1.944)*10)/10)..'节\n============',5)
                    self.playerSpeedHint = Range.playerSpeedHintRepeatTime
                end
            end
            self.playerSpeedHint = self.playerSpeedHint - repeatTime

            -- 玩家飞行速度低于30节
            if playerSpeed < 30/1.944 then
                self.EndTraining({context = self,reason = '玩家速度小于 30 节',Error = true})
                return
            end
        end

        return time + repeatTime
    end

    function Range:Start(Delay,RepeatScanSeconds)
        local tDelay = Delay or 1
        local RepeatScanInterval = RepeatScanSeconds or Range.MonitorRepeatTime

        if self.MonitorID then
            self:Stop()
        end

        self.MonitorID = timer.scheduleFunction(self._MonitorFunction,{context = self,repeatTime = RepeatScanInterval},timer.getTime() + tDelay)
    end
    
    function Range:Stop()
        if not self.MonitorID then return end

        timer.removeFunction(self.MonitorID)
        self.MonitorID = nil
    end


    --- Init
    for i=1,10,1 do
        local rangeZone = trigger.misc.getZone('RangeZone-'..i)
        if rangeZone then
            local newRange = Range:New(rangeZone)
            for j=1,20,1 do
                local tempGroupName = 'Blue-Template-'..i
                Range.AddTargetType(tempGroupName,coalition.side.BLUE)
            end
            for j=1,20,1 do
                local tempGroupName = 'Red-Template-'..i
                Range.AddTargetType(tempGroupName,coalition.side.RED)
            end
        end
    end
end